Introduce API Tokens with cluster_permissions and index_permissions directly associated with the token#5443
Conversation
…4921) Signed-off-by: Derek Ho <dxho@amazon.com>
…ect#4967) Signed-off-by: Derek Ho <dxho@amazon.com>
Signed-off-by: Derek Ho <dxho@amazon.com>
Signed-off-by: Derek Ho <dxho@amazon.com>
Signed-off-by: Derek Ho <dxho@amazon.com>
…00 tokens outstanding (opensearch-project#5147) Signed-off-by: Derek Ho <dxho@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
|
Persistent review updated to latest commit 78049ef |
Signed-off-by: Craig Perkins <cwperx@amazon.com>
PR Code Analyzer ❗AI-powered 'Code-Diff-Analyzer' found issues on commit 8b619cc.
The table above displays the top 10 most important findings. Pull Requests Author(s): Please update your Pull Request according to the report above. Repository Maintainer(s): You can Thanks. |
|
Persistent review updated to latest commit 8b619cc |
Signed-off-by: Craig Perkins <cwperx@amazon.com>
|
Persistent review updated to latest commit f0772a5 |
Signed-off-by: Craig Perkins <cwperx@amazon.com>
|
Persistent review updated to latest commit 39b5609 |
Signed-off-by: Craig Perkins <cwperx@amazon.com>
|
Persistent review updated to latest commit d85f1eb |
Signed-off-by: Craig Perkins <cwperx@amazon.com>
|
Persistent review updated to latest commit a85a39e |
|
Persistent review updated to latest commit 67105b5 |
nibix
left a comment
There was a problem hiding this comment.
I checked out the integration with the new privilege evaluator. There are a few things to be considered, see below. If you'd need more background on this, we can have a quick conversation.
…s/security into feature/api-tokens-cwperx
…TOKEN_WRITE Signed-off-by: Craig Perkins <cwperx@amazon.com>
|
Persistent review updated to latest commit 5ead3fc |
|
Failed to generate code suggestions for PR |
Signed-off-by: Craig Perkins <cwperx@amazon.com>
|
Persistent review updated to latest commit 42ecbe0 |
DarshitChanpura
left a comment
There was a problem hiding this comment.
This is some great work! @cwperks .
I left a few comments and clarification questions.
| static { | ||
| PARSER.declareString(constructorArg(), new ParseField(NAME_FIELD)); | ||
| PARSER.declareString(constructorArg(), new ParseField(TOKEN_HASH_FIELD)); | ||
| PARSER.declareStringArray(optionalConstructorArg(), new ParseField(CLUSTER_PERMISSIONS_FIELD)); |
There was a problem hiding this comment.
what is the purpose of making cluster permissions optional too?
There was a problem hiding this comment.
Because then it allows having narrowly scoped tokens. i.e. Search on indexA only.
^ why have the caller pass cluster_permissions: [] unnecessarily?
| apiTokenRepository.getTokenCount(ActionListener.wrap(tokenCount -> { | ||
| ConfigV7 config = configurationRepository.getConfiguration(CType.CONFIG).getCEntry(CType.CONFIG.name()); | ||
| int maxTokens = config.dynamic.api_tokens.getMaxTokens(); | ||
| if (tokenCount >= maxTokens) { |
There was a problem hiding this comment.
at some point in future, we should add a schedule job to cleanup expired/revoked tokens.
There was a problem hiding this comment.
Why? They are retained for auditability. Cluster admins wanted to delete metadata from revoked tokens can do so using admin cert auth.
| client.threadPool().getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); | ||
|
|
||
| SearchRequest searchRequest = new SearchRequest(ConfigConstants.OPENSEARCH_API_TOKENS_INDEX); | ||
| searchRequest.source(new SearchSourceBuilder().size(10_000)); |
There was a problem hiding this comment.
we should add a filter to not get revoked/expired tokens?
There was a problem hiding this comment.
@finnegancarroll had a similar comment above. I will address if the maintainers would like, but IMO its not necessary. This is done because of the UX in security-dashboards-plugin where it will show revoked and expired tokens..similar to Github with personal access tokens. It won't be supported with just this PR, but we should support renewing tokens with the same metadata in the future.
| } | ||
| } | ||
|
|
||
| // SHA-256 is sufficient for hashing high-entropy random tokens. Consider making configurable if algorithm rotation is needed. |
There was a problem hiding this comment.
we should consider this in next-iteration 😅
There was a problem hiding this comment.
Hence the comment :). If anyone in the community needs more flexibility then it should be obvious where to add it.
| apiTokenRepository.reloadApiTokensFromIndex( | ||
| ActionListener.wrap( | ||
| unused -> log.debug("API tokens loaded on node start"), | ||
| e -> log.warn("Failed to load API tokens on node start", e) |
There was a problem hiding this comment.
is there a mechanism where admin would know without looking at this warning?
maybe we show it in dashboard?
| import static org.hamcrest.Matchers.equalTo; | ||
| import static org.junit.Assert.assertEquals; | ||
|
|
||
| public class ApiTokenTest { |
There was a problem hiding this comment.
we should add a test for testing all the optional arguments.
There was a problem hiding this comment.
There's a mix of tests throughout that test different permissions payloads that may or may not include cluster_permissions or index_permissions. I will double check.
| public AuditLogsRule auditLogsRule = new AuditLogsRule(); | ||
|
|
||
| @Test | ||
| public void testApiTokenAuthenticationIsAudited() { |
There was a problem hiding this comment.
we should also add a test about any changes to the token being audited.
There was a problem hiding this comment.
Added a test for revoke and corresponding entry
Signed-off-by: Craig Perkins <cwperx@amazon.com>
|
Persistent review updated to latest commit aaf3fc4 |
Description
Re-basing #5225 with the latest changes from
main.This PR introduces API Tokens — a new capability in the Security plugin that allows security admins to issue long-lived, scoped tokens and associate permissions directly with the token.
How it works
API Tokens are opaque tokens with the format
os_<random>. When a token is created, a SHA-256 hash of the plaintext token is stored in a system index,.opensearch_security_api_tokens. The plaintext token is returned once at creation time and never stored. On each request, the incoming token is hashed and looked up in an in-memory cache populated from the index.Tokens are authenticated via the
Authorization: ApiKey <token>header.What is novel about this approach compared to OBO tokens is that permissions are scoped directly to the token rather than derived from the issuing user's roles. An admin can issue a token with only the permissions it needs — for example, read-only access to a single index — regardless of the admin's own permissions. This enforces the principle of least privilege and is a key building block toward deprecating Roles Injection, the current practice for how plugins run async jobs with user-scoped permissions.
Revocation model
Tokens use a soft-delete revocation model. When a token is revoked via
DELETE /_plugins/_security/api/apitokens/{id}, the document in the index is updated with arevoked_attimestamp rather than being deleted. This means:revoked_atfield, enabling UIs to display revocation history and audit trails.revoked_atset are excluded from the in-memory authentication maps, so they cannot be used to authenticate.Token Identity & Naming
Token names serve as the user-facing identity. They must be unique and match
[a-zA-Z0-9_-]+. In audit logs and internal user contexts, tokens appear astoken:<name>(e.g.,token:my-service-token). The SHA-256 hash used for authentication lookup is internal-only and never exposed in API responses or audit logs.System Index Protection
API tokens are denied access to all system indices regardless of their granted permissions. This is enforced at the privilege evaluation layer via
SpecialIndexProtectionand applies to both legacy and V4 privilege evaluation modes.API Reference
Create API Token
POST /_plugins/_security/api/apitokensThe create request accepts
duration_seconds— how long the token lives, in seconds. The cluster settingmax_duration_secondscontrols the maximum allowed duration.Request:
{ "name": "my-token", "cluster_permissions": ["cluster:monitor/health"], "index_permissions": [ { "index_pattern": ["logs-*"], "allowed_actions": ["indices:data/read/search"] } ], "duration_seconds": 3600 }Response:
{ "id": "Nd_pMRWeAC93ZGMhRa5CxX", "token": "os_abc123..." }The
idis used to manage the token, such as listing or revoking it. The plaintext token is returned once and never stored — save it immediately.List API Tokens
GET /_plugins/_security/api/apitokensReturns all tokens, including revoked ones. Revoked tokens include a
revoked_atfield (epoch millis). Theexpires_atfield is the epoch millis timestamp when the token expires.Response:
[ { "id": "Nd_pMRWeAC93ZGMhRa5CxX", "name": "my-token", "iat": 1742000000000, "expires_at": 1742003600000, "cluster_permissions": ["cluster:monitor/health"], "index_permissions": [ { "index_pattern": ["logs-*"], "allowed_actions": ["indices:data/read/search"] } ] }, { "id": "Xf_qNSZeBC04AHNiSb6DyY", "name": "old-token", "iat": 1741000000000, "expires_at": 1741003600000, "revoked_at": 1741500000000, "cluster_permissions": ["cluster:monitor/health"], "index_permissions": [] } ]Revoke API Token
DELETE /_plugins/_security/api/apitokens/{id}Response:
{ "message": "Token Nd_pMRWeAC93ZGMhRa5CxX revoked successfully." }Revocation is a soft-delete — the token metadata is retained with a
revoked_attimestamp. The token is immediately unusable after the response is returned. The cache refresh is broadcast synchronously to all nodes before the response is sent.Using a Token
Pass the token in the
Authorizationheader using theApiKeyscheme:Authorization: ApiKey os_abc123...Example — search a permitted index:
Response:
{ "hits": { "total": { "value": 3, "relation": "eq" }, "hits": [ ... ] } }Example — attempt a forbidden action:
Response:
{ "error": { "type": "security_exception", "reason": "no permissions for [indices:admin/delete]" }, "status": 403 }Issues Resolved
Partially resolves #4009, limited to security admins in the initial release.
Check List
--signoff